/* @flow */

import {
  tip,
  toArray,
  hyphenate,
  handleError,
  formatComponentName
} from '../util/index'
import { updateListeners } from '../vdom/helpers/index'

export function initEvents (vm: Component) {
  //该对象用于存放在当前Vue实例上设置的需要通过$emit方法触发的事件，
  //其中事件名作为键，回调函数作为值
  vm._events = Object.create(null)
  //标记当前vue实例是否添加了生命周期钩子函数的事件监听，用于在
  //Vue实例触发相应的生命周期钩子时，可以触发相应的事件，该属性只在lifecycle.js的callHook方法中使用。
  vm._hasHookEvent = false
  // init parent attached events
  //_parentListeners属性是在core/instance/create-component.js-->createComponentInstanceForVnode方法中设置的，
  //在创建子组件时注入。保存着父组件向子组件中设置的监听。注意，_parentListeners保存的是通过data中的on添加的，而不包括nativeOn中的监听。
  //nativeOn中保存的是原生的事件监听（注意，添加原生事件，需要使用.native修饰），
  //而on对于组件来说，保存的是需要通过$emit方法触发的事件监听。
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

let target: Component

function add (event, fn, once) {
  if (once) {
    target.$once(event, fn)
  } else {
    target.$on(event, fn)
  }
}

function remove (event, fn) {
  target.$off(event, fn)
}

//该方法用于根据最新的监听对象列表与旧的监听函数列表进行对比，
//在Vue实例上对监听进行增删改等
//注意，这里添加的监听是组件特有的监听，在data的on属性中添加，需要通过$emit方法触发。
export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, vm)
}

export function eventsMixin (Vue: Class<Component>) {
  //组件的生命周期钩子函数被调用时，会触发相应的事件，比如hook:created等
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$on(event[i], fn)
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

  Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on () {
      //在调用事件回调函数之前，删除对应的事件监听函数，以此实现
      //事件只会被调用一次。
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    //这里通过fn属性保存监听函数，主要是为了在移除事件时，可以通过fn与待移除的
    //函数进行比较
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // all 移除所有的事件
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    const cbs = vm._events[event]
    if (!cbs) {
      return vm
    }
    if (arguments.length === 1) {
      vm._events[event] = null
      return vm
    }
    // specific handler
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      //cb.fn === fn是为了支持$once函数中的移除
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {
      //这里的判断说明：
      //1）在Vue的HTML模板中，在组件上通过@xxx的形式添加的事件监听，在组件内部，也是通过$on添加的事件监听
      //2）在页面的HTML中的属性需要写成连字符形式，所以在触发事件时，也需要写成连字符形式
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      for (let i = 0, l = cbs.length; i < l; i++) {
        try {
          //对于直接通过$on方法添加的事件，cbs中的函数的执行上下文是当前vue实例，如果是通过@xxx的形式添加的事件，
          //则cbs中的函数的执行上下文是该函数声明的vue实例，而不是当前vue实例，因为在其声明的实例中已通过bind方法
          //绑定了上下文，所以这里指定了vm实例也无效。
          cbs[i].apply(vm, args)
        } catch (e) {
          handleError(e, vm, `event handler for "${event}"`)
        }
      }
    }
    return vm
  }
}
